/*
 * Copyright (C) 2009 Google Inc. All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are
 * met:
 *
 *     * Redistributions of source code must retain the above copyright
 * notice, this list of conditions and the following disclaimer.
 *     * Redistributions in binary form must reproduce the above
 * copyright notice, this list of conditions and the following disclaimer
 * in the documentation and/or other materials provided with the
 * distribution.
 *     * Neither the name of Google Inc. nor the names of its
 * contributors may be used to endorse or promote products derived from
 * this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

UI.PopoverHelper = class {
  /**
   * @param {!Element} container
   * @param {function(!MouseEvent):?UI.PopoverRequest} getRequest
   */
  constructor(container, getRequest) {
    this._disableOnClick = false;
    this._hasPadding = false;
    this._getRequest = getRequest;
    this._scheduledRequest = null;
    /** @type {?function()} */
    this._hidePopoverCallback = null;
    this._container = container;
    this._showTimeout = 0;
    this._hideTimeout = 0;
    /** @type {?number} */
    this._hidePopoverTimer = null;
    /** @type {?number} */
    this._showPopoverTimer = null;
    this._boundMouseDown = this._mouseDown.bind(this);
    this._boundMouseMove = this._mouseMove.bind(this);
    this._boundMouseOut = this._mouseOut.bind(this);
    this._container.addEventListener('mousedown', this._boundMouseDown, false);
    this._container.addEventListener('mousemove', this._boundMouseMove, false);
    this._container.addEventListener('mouseout', this._boundMouseOut, false);
    this.setTimeout(1000);
  }

  /**
   * @param {number} showTimeout
   * @param {number=} hideTimeout
   */
  setTimeout(showTimeout, hideTimeout) {
    this._showTimeout = showTimeout;
    this._hideTimeout = typeof hideTimeout === 'number' ? hideTimeout : showTimeout / 2;
  }

  /**
   * @param {boolean} hasPadding
   */
  setHasPadding(hasPadding) {
    this._hasPadding = hasPadding;
  }

  /**
   * @param {boolean} disableOnClick
   */
  setDisableOnClick(disableOnClick) {
    this._disableOnClick = disableOnClick;
  }

  /**
   * @param {!Event} event
   * @return {boolean}
   */
  _eventInScheduledContent(event) {
    return this._scheduledRequest ? this._scheduledRequest.box.contains(event.clientX, event.clientY) : false;
  }

  /**
   * @param {!Event} event
   */
  _mouseDown(event) {
    if (this._disableOnClick) {
      this.hidePopover();
      return;
    }
    if (this._eventInScheduledContent(event))
      return;

    this._startHidePopoverTimer(0);
    this._stopShowPopoverTimer();
    this._startShowPopoverTimer(/** @type {!MouseEvent} */ (event), 0);
  }

  /**
   * @param {!Event} event
   */
  _mouseMove(event) {
    // Pretend that nothing has happened.
    if (this._eventInScheduledContent(event))
      return;

    this._startHidePopoverTimer(this._hideTimeout);
    this._stopShowPopoverTimer();
    if (event.which && this._disableOnClick)
      return;
    this._startShowPopoverTimer(
        /** @type {!MouseEvent} */ (event), this.isPopoverVisible() ? this._showTimeout * 0.6 : this._showTimeout);
  }

  /**
   * @param {!Event} event
   */
  _popoverMouseMove(event) {
    this._stopHidePopoverTimer();
  }

  /**
   * @param {!UI.GlassPane} popover
   * @param {!Event} event
   */
  _popoverMouseOut(popover, event) {
    if (!popover.isShowing())
      return;
    if (event.relatedTarget && !event.relatedTarget.isSelfOrDescendant(popover.contentElement))
      this._startHidePopoverTimer(this._hideTimeout);
  }

  /**
   * @param {!Event} event
   */
  _mouseOut(event) {
    if (!this.isPopoverVisible())
      return;
    if (!this._eventInScheduledContent(event))
      this._startHidePopoverTimer(this._hideTimeout);
  }

  /**
   * @param {number} timeout
   */
  _startHidePopoverTimer(timeout) {
    // User has |timeout| ms to reach the popup.
    if (!this._hidePopoverCallback || this._hidePopoverTimer)
      return;

    this._hidePopoverTimer = setTimeout(() => {
      this._hidePopover();
      this._hidePopoverTimer = null;
    }, timeout);
  }

  /**
   * @param {!MouseEvent} event
   * @param {number} timeout
   */
  _startShowPopoverTimer(event, timeout) {
    this._scheduledRequest = this._getRequest.call(null, event);
    if (!this._scheduledRequest)
      return;

    this._showPopoverTimer = setTimeout(() => {
      this._showPopoverTimer = null;
      this._stopHidePopoverTimer();
      this._hidePopover();
      this._showPopover(event.target.ownerDocument);
    }, timeout);
  }

  _stopShowPopoverTimer() {
    if (!this._showPopoverTimer)
      return;
    clearTimeout(this._showPopoverTimer);
    this._showPopoverTimer = null;
  }

  /**
   * @return {boolean}
   */
  isPopoverVisible() {
    return !!this._hidePopoverCallback;
  }

  hidePopover() {
    this._stopShowPopoverTimer();
    this._hidePopover();
  }

  _hidePopover() {
    if (!this._hidePopoverCallback)
      return;
    this._hidePopoverCallback.call(null);
    this._hidePopoverCallback = null;
  }

  /**
   * @param {!Document} document
   */
  _showPopover(document) {
    const popover = new UI.GlassPane();
    popover.registerRequiredCSS('ui/popover.css');
    popover.setSizeBehavior(UI.GlassPane.SizeBehavior.MeasureContent);
    popover.setMarginBehavior(UI.GlassPane.MarginBehavior.Arrow);
    const request = this._scheduledRequest;
    request.show.call(null, popover).then(success => {
      if (!success)
        return;

      if (this._scheduledRequest !== request) {
        if (request.hide)
          request.hide.call(null);
        return;
      }

      // This should not happen, but we hide previous popover to be on the safe side.
      if (UI.PopoverHelper._popoverHelper) {
        console.error('One popover is already visible');
        UI.PopoverHelper._popoverHelper.hidePopover();
      }
      UI.PopoverHelper._popoverHelper = this;

      popover.contentElement.classList.toggle('has-padding', this._hasPadding);
      popover.contentElement.addEventListener('mousemove', this._popoverMouseMove.bind(this), true);
      popover.contentElement.addEventListener('mouseout', this._popoverMouseOut.bind(this, popover), true);
      popover.setContentAnchorBox(request.box);
      popover.show(document);

      this._hidePopoverCallback = () => {
        if (request.hide)
          request.hide.call(null);
        popover.hide();
        delete UI.PopoverHelper._popoverHelper;
      };
    });
  }

  _stopHidePopoverTimer() {
    if (!this._hidePopoverTimer)
      return;
    clearTimeout(this._hidePopoverTimer);
    this._hidePopoverTimer = null;

    // We know that we reached the popup, but we might have moved over other elements.
    // Discard pending command.
    this._stopShowPopoverTimer();
  }

  dispose() {
    this._container.removeEventListener('mousedown', this._boundMouseDown, false);
    this._container.removeEventListener('mousemove', this._boundMouseMove, false);
    this._container.removeEventListener('mouseout', this._boundMouseOut, false);
  }
};

/** @typedef {{box: !AnchorBox, show:(function(!UI.GlassPane):!Promise<boolean>), hide:(function()|undefined)}} */
UI.PopoverRequest;
